Skip to content

fix(ui): read SwapKit's error codes on the DEX amount screens - #1087

Open
romchornyi wants to merge 2 commits into
developfrom
fix/swapkit-error-mapping
Open

fix(ui): read SwapKit's error codes on the DEX amount screens#1087
romchornyi wants to merge 2 commits into
developfrom
fix/swapkit-error-mapping

Conversation

@romchornyi

@romchornyi romchornyi commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Issue being fixed or feature implemented

Reported from the Dash DEX Enter amount screens: the errors SwapKit returns had changed, and the screen answered almost everything with the same dead end — "Something went wrong setting up your swap" — even when the API had said exactly what was wrong.

Verified by hand against api.swapkit.dev on 2026-08-28. An amount below a route's floor comes back three different ways depending on how far below it is:

Request (NEAR, DASH → BTC) Response
0.05 DASH 200 + providerErrors[0].errorCode = sellAssetAmountTooSmall
≤ 0.01 DASH 404 + error = noRoutesFound
≥ 1000 DASH 200 + providerErrors[0].errorCode = apiRequestFailed

Two reasons none of it reached the user:

  1. decodeQuoteError returned body.message before body.error, so the prose reached the mapper and the code never did. "No routes found for DASH.DASH -> BTC.BTC" does not match noRoutesFound, and the buy screen's own contains("noRoutesFound") check missed it for the same reason — so even the one error that screen tried to handle fell through to the generic copy.
  2. A failure a provider reports — an HTTP 200 with no routes and a providerErrors[] entry — was passed on as its prose alone, dropping errorCode, the only stable identifier in the response.

What was done?

Mirrors Android's SwapKitErrors (dashpay/dash-wallet#1526) and the enter-amount copy from dashpay/dash-wallet#1539.

  • providerErrorMessage(_:) renders a provider failure as "<code>: <detail>", so provider-level and top-level failures reach the mapper through one path.
  • Below-minimum codes are family-matched on an AmountTooSmall / AmountTooLow suffix rather than enumerated — SwapKit doesn't document the per-provider vocabulary, and the prefix names whichever side was too small. Both forms carry "Amount", so an unrelated below-threshold code (a too-low fee) stays out.
  • Four live codes mapped that previously fell through: apiRequestFailed, invalidRoute, invalidAsset, memoTooLongForSourceChain. The last one shares the copy the wallet already shows when it catches an over-length memo locally before broadcasting.
  • Routability probing classifies on codes too. A below-minimum reply says the probe amount was too small, not that the asset can't be routed — the coin picker no longer marks an asset unroutable on that evidence. A provider naming its own noRoutesFound still counts as conclusive.
  • decodeQuoteError is code-first, matching what the swap-side decoder already did.
  • Over-balance copy follows the redesign: "The maximum transaction amount is $205.32" → "Max $205.32" (Figma 24034:44864, Android maya_max_amount_error).

One string is removed: the buy screen's hand-rolled dex_enter_amount_invalid fallback is orphaned now that both amount screens share the mapper.

Not in this PR

How Has This Been Tested?

  • Clean dashpay build, ARCHS=arm64, iOS Simulator SDK — BUILD SUCCEEDED.
  • Every error shape above was reproduced against live api.swapkit.dev with the app's own API key and request bodies (quote and swap, both directions, amount sweeps per asset), and the mapping checked against those recorded responses.
  • Not exercised on device end-to-end: with Maya down, a Maya-routed sell can't be run right now, and the NEAR sell path is separately blocked upstream (/v3/swap answers invalidRoute for DASH → BTC even with SwapKit's own nextActions payload).

Breaking Changes

None.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

Summary by CodeRabbit

  • Bug Fixes
    • Improved swap error handling and messaging across buy and convert flows.
    • More accurately identifies unavailable routes and amounts below provider minimums.
    • Displays clearer provider and API error details when quotes or routes fail.
    • Corrected maximum transaction amount messaging.
  • Localization
    • Added dedicated messages for amounts below the minimum and maximum transaction limits.

Every amount-related failure on Enter amount rendered as a dead end
("Something went wrong setting up your swap") even when SwapKit had said
exactly what was wrong. Two causes, both about where the code was read
from.

`decodeQuoteError` returned `body.message` before `body.error`, so the
prose reached the mapper and the code never did: "No routes found for
DASH.DASH -> BTC.BTC" does not match `noRoutesFound`, and the buy
screen's own `contains("noRoutesFound")` check missed it for the same
reason. And a failure a provider reports — an HTTP 200 with no routes
and `providerErrors[]` — was passed on as its prose alone, dropping
`errorCode`, the only stable identifier in the response.

Measured against api.swapkit.dev on 2026-08-28: at 0.05 DASH a
DASH -> BTC quote answers 200 with `sellAssetAmountTooSmall`; below
about 0.01 DASH it answers 404 `noRoutesFound`; above the pool's depth
it answers 200 with `apiRequestFailed`. All three read as "something
went wrong".

- `providerErrorMessage` puts the code back in front of the prose, so
  provider-level and top-level failures reach the mapper the same way.
- Below-minimum codes are family-matched on an `AmountTooSmall` /
  `AmountTooLow` suffix rather than enumerated, since SwapKit does not
  document the per-provider vocabulary.
- `apiRequestFailed`, `invalidRoute`, `invalidAsset` and
  `memoTooLongForSourceChain` are mapped; the last shares the copy the
  wallet already uses when it catches an over-length memo locally.
- Routability probing classifies on codes too: a below-minimum reply
  says the probe amount was too small, not that the asset is unroutable,
  so the coin picker no longer hides an asset on that evidence.
- The convert screen's over-balance line follows the redesign to
  "Max $205.32" (Figma 24034:44864).

Mirrors Android's `SwapKitErrors` (dashpay/dash-wallet#1526) and the
enter-amount copy from dashpay/dash-wallet#1539.
@coderabbitai

coderabbitai Bot commented Aug 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

Next included review available in 50 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: dc6991d6-4adb-41f5-bb80-8f4f492ab38b

📥 Commits

Reviewing files that changed from the base of the PR and between c5eea1e and 46ebf78.

📒 Files selected for processing (3)
  • DashWallet/Sources/Models/Swap/SwapKitErrorCopy.swift
  • DashWallet/Sources/Models/SwapKit/SwapKitSwapProvider.swift
  • DashWalletTests/SwapKitQuoteDecodingTests.swift

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4c68dc50-705e-4fc2-8fd7-bbd1c3868a08

📥 Commits

Reviewing files that changed from the base of the PR and between ed88d63 and c5eea1e.

📒 Files selected for processing (5)
  • DashWallet/Sources/Models/Swap/SwapKitErrorCopy.swift
  • DashWallet/Sources/Models/SwapKit/SwapKitSwapProvider.swift
  • DashWallet/Sources/UI/Swap/Buy/EnterAmount/BuyEnterAmountViewModel.swift
  • DashWallet/Sources/UI/Swap/Convert/SwapConvertViewModel.swift
  • DashWallet/en.lproj/Localizable.strings

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

SwapKit errors now use normalized, case-insensitive codes for provider formatting, route classification, and amount validation. Buy and convert flows use shared error mapping. Localization adds below-minimum and shortened maximum-amount messages.

Changes

SwapKit error flow

Layer / File(s) Summary
Normalize and map SwapKit errors
DashWallet/Sources/Models/Swap/SwapKitErrorCopy.swift
SwapKitErrorCopy now extracts normalized error codes, formats provider errors, detects no-route and below-minimum failures, and maps apiRequestFailed and invalidRoute.
Apply normalized errors to provider flows
DashWallet/Sources/Models/SwapKit/SwapKitSwapProvider.swift
Quote and buy-route flows use shared provider formatting. Routability now uses provider error codes instead of prose matching. Quote decoding prefers top-level error codes.
Use mapped messages in swap UI
DashWallet/Sources/UI/Swap/Buy/EnterAmount/BuyEnterAmountViewModel.swift, DashWallet/Sources/UI/Swap/Convert/SwapConvertViewModel.swift, DashWallet/en.lproj/Localizable.strings
Buy and convert error paths use SwapKitErrorCopy.message(for:coin:). Localization adds below-minimum copy and changes the maximum-amount format to Max %@.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to c5eea

The PR improves how existing SwapKit errors are classified and shown on swap amount screens without changing transaction authority, stored state, or external interfaces; no actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant SwapKitSwapProvider
  participant SwapKitErrorCopy
  participant BuyEnterAmountViewModel
  participant SwapConvertViewModel
  SwapKitSwapProvider->>SwapKitErrorCopy: Normalize provider error
  SwapKitErrorCopy-->>SwapKitSwapProvider: Return error code and routability
  SwapKitSwapProvider->>BuyEnterAmountViewModel: Return quote error
  BuyEnterAmountViewModel->>SwapKitErrorCopy: Map validation message
  SwapKitSwapProvider->>SwapConvertViewModel: Return API error
  SwapConvertViewModel->>SwapKitErrorCopy: Map conversion message
Loading

Suggested reviewers: jeanpierreroma, llbartekll

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.58% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 19 functions across 4 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: updating DEX amount screens to read and use SwapKit error codes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 31.58% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 19 functions across 4 files. (1 skipped: 1 unsupported.)

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/swapkit-error-mapping

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thepastaclaw

thepastaclaw commented Aug 31, 2026

Copy link
Copy Markdown

🕓 Ready for review — 28 ahead in queue (commit 46ebf78)
Queue position: 29/58 · 2 reviews active
ETA: start ~08:01 UTC · complete ~08:56 UTC (median 54m across 30 recent reviews; 2 slots)
Queued 2d 20h ago · Last checked: 2026-09-05 19:10 UTC

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final validation — GLM Flash + Sol

The code-first normalization correctly preserves stable SwapKit codes and improves the amount-screen copy. Two in-scope suggestions remain: top-level noRoutesFound is still treated as conclusive despite the PR's measured amount ambiguity, and the new normalization and classification behavior lacks regression coverage; two minor documentation and cleanup issues are also noted.

Source: reviewer 1: glm-5.3-flash (agent: phase1-reviewer, role: general); reviewer 2: gpt-5.6-sol (agent: phase2-reviewer, role: general); final verifier: gpt-5.6-sol (agent: sol-verifier, role: final-verifier)

Review provenance

  • Phase 1 reviewers (GLM Flash): glm-5.3-flash — general (completed); agent phase1-reviewer
  • Fresh verifier (Sol): gpt-5.6-sol — final-verifier; agent sol-verifier
  • Phase 2 reviewers (Sol): gpt-5.6-sol — general (completed); agent phase2-reviewer

🟡 2 suggestion(s) | 💬 2 nitpick(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `DashWallet/Sources/Models/SwapKit/SwapKitSwapProvider.swift`:
- [SUGGESTION] DashWallet/Sources/Models/SwapKit/SwapKitSwapProvider.swift:749-751: Do not classify top-level `noRoutesFound` as conclusively unroutable
  The PR's recorded API behavior shows that SwapKit returns top-level `noRoutesFound` when an amount is far below a route's minimum. This branch nevertheless caches `.notRoutable` for ten minutes, so an asset can disappear from the optimistic Buy picker when its approximately $50 probe—or the one-unit fallback—falls below that asset's route floor. Remove this branch and let the response fall through to `nil`; explicit provider below-minimum errors can remain positive routability evidence, while actual routes remain conclusive positive proof.

In `DashWallet/Sources/Models/Swap/SwapKitErrorCopy.swift`:
- [SUGGESTION] DashWallet/Sources/Models/Swap/SwapKitErrorCopy.swift:40-79: Add regression tests for the new SwapKit error normalization
  This bug fix introduces several interacting parsing and classification rules without executable coverage. Extend the existing SwapKit tests with compile-ready cases for provider code/detail composition, code-only and detail-only responses, whitespace and nil handling, code-first top-level decoding, case-insensitive `AmountTooSmall` and `AmountTooLow` suffixes, the four newly mapped codes, unknown-code fallback, and routability outcomes for explicit below-minimum versus ambiguous `noRoutesFound` responses. These tests are needed to prevent a decoder or mapper refactor from dropping the stable code behind provider prose again.
- [NITPICK] DashWallet/Sources/Models/Swap/SwapKitErrorCopy.swift:38: Correct the nonexistent mapper reference in the doc comment
  The comment references `message(for:coin:minimum:)`, but this module only defines `message(for:coin:)`. Point the documentation at the actual mapper so readers are not directed to a nonexistent overload.
- [NITPICK] DashWallet/Sources/Models/Swap/SwapKitErrorCopy.swift:55-65: Remove the unused `isAmountTooLow(_:)` helper
  Repository-wide usage shows no caller for this newly added helper. The amount screens call `message(for:coin:)`, while routability intentionally uses the narrower `isNoRoute(_:)` and `isBelowMinimum(_:)` methods. Delete the unused API and move any useful ambiguity explanation to the classifier that implements the behavior.

Comment on lines 749 to 751
if SwapKitErrorCopy.isNoRoute(response.error) {
return .notRoutable
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Do not classify top-level noRoutesFound as conclusively unroutable

The PR's recorded API behavior shows that SwapKit returns top-level noRoutesFound when an amount is far below a route's minimum. This branch nevertheless caches .notRoutable for ten minutes, so an asset can disappear from the optimistic Buy picker when its approximately $50 probe—or the one-unit fallback—falls below that asset's route floor. Remove this branch and let the response fall through to nil; explicit provider below-minimum errors can remain positive routability evidence, while actual routes remain conclusive positive proof.

source: ['codex']

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 46ebf78 — the top-level branch is gone and that response now falls through to nil.

Agreed on the reasoning, and the recorded data is worse than the comment suggests: DASH → BTC returns top-level noRoutesFound at 0.01 DASH and a route at 0.3, so the ambiguous band is wide. The probe’s one-unit fallback (no cached USD price) lands inside it for any cheap asset.

Provider-level evidence still decides: a below-minimum code is positive, a provider naming its own noRoutesFound in providerErrors[] is the one conclusive negative — it answers for the single provider the probe asked about — and anything else (apiRequestFailed) leaves the question open. The optimistic-filter comment above candidates.filter was promising pruning the top-level code no longer performs, so it was corrected too.

Comment on lines +40 to +79
static func providerErrorMessage(_ error: SwapKitProviderError?) -> String? {
let code = error?.errorCode?.trimmingCharacters(in: .whitespacesAndNewlines)
let detail = error?.message?.trimmingCharacters(in: .whitespacesAndNewlines)
switch (code?.isEmpty == false ? code : nil, detail?.isEmpty == false ? detail : nil) {
case let (code?, detail?):
return "\(code): \(detail)"
case let (code?, nil):
return code
case let (nil, detail?):
return detail
case (nil, nil):
return nil
}
}

/// True when the failure means "the sell amount is under what this route can fill" — the case
/// the amount screens surface inline (raise the amount and retry) instead of as a dead end.
/// `noRoutesFound` is included because SwapKit answers with it for amounts far below the
/// minimum and only switches to an explicit below-minimum code close to the floor: measured
/// 2026-08-28, DASH → BTC returned `noRoutesFound` at 0.01 DASH and `sellAssetAmountTooSmall`
/// (min 0.175) at 0.05. It is genuinely ambiguous — a route can also be briefly unavailable —
/// so its copy stays neutral.
static func isAmountTooLow(_ rawError: String?) -> Bool {
let code = code(of: rawError)
return code == noRoutesFoundCode || isBelowMinimumCode(code)
}

static func message(for rawError: String?, coin: SwapCryptoCurrency) -> String {
let code = rawError?
.components(separatedBy: ":")
.first?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
?? ""
let code = code(of: rawError)

// Per-provider below-minimum codes are family-matched rather than enumerated: SwapKit does
// not document the per-provider vocabulary, and the prefix names whichever side was too
// small (`sellAssetAmountTooSmall` today). Both forms carry "amount", so an unrelated
// below-threshold code — a too-low fee, say — stays out.
if isBelowMinimumCode(code) {
return NSLocalizedString(
"This amount is below the minimum for this swap. Please enter a larger amount.",
comment: "Dash DEX / dex_error_amount_too_small"
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Add regression tests for the new SwapKit error normalization

This bug fix introduces several interacting parsing and classification rules without executable coverage. Extend the existing SwapKit tests with compile-ready cases for provider code/detail composition, code-only and detail-only responses, whitespace and nil handling, code-first top-level decoding, case-insensitive AmountTooSmall and AmountTooLow suffixes, the four newly mapped codes, unknown-code fallback, and routability outcomes for explicit below-minimum versus ambiguous noRoutesFound responses. These tests are needed to prevent a decoder or mapper refactor from dropping the stable code behind provider prose again.

source: ['claude', 'codex']

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 46ebf78 — a SwapKitErrorCopyTests case alongside the existing decoding tests, 12 methods covering: code-before-prose composition and its code-only / detail-only / blank / nil fallbacks; the case-insensitive AmountTooSmall / AmountTooLow suffix family, including the composed "<code>: <detail>" form and a negative for inboundFeeTooLow; isNoRoute on both forms; the four newly mapped codes; memoTooLongForSourceChain sharing the local memo copy; detail-after-code not changing the mapping; unknown and empty falling back to generic.

The anchor case is the regression itself: message(for: "No routes found for DASH.DASH -> BTC.BTC") must still be generic. The fix is that callers stopped sending prose alone, not that the mapper started guessing from text — a test asserting the prose maps would cement exactly the wrong behaviour.

Two limits worth stating plainly:

  • Not executed. The test target cannot be built on this machine: the ../platform checkout it takes SwiftDashSDK from sits on a side branch that predates several symbols develop needs (ManagedPlatformWallet.ParsedIdentityUpdateTransition, PlatformWalletManager.trackedMasternodes, MasternodeKeyRole), and DashWalletTests/DashConnect/PlatformDashConnectDataSourceTests.swift references the first of those directly. That is unrelated to this PR and pre-dates it. To get a real signal I extracted SwapKitErrorCopy verbatim into a standalone target with stubbed SwapKitProviderError / SwapCryptoCurrency / DWLogger and ran the same 28 assertions these tests encode — all pass. Whoever has an up-to-date ../platform should run the XCTest cases before merge.
  • Two call sites stay covered only indirectly. routability(from:) and decodeQuoteError(from:) are private, and reaching them would mean widening the access of a private nested enum and a method purely for tests. Every rule they depend on (providerErrorMessage, isNoRoute, isBelowMinimum, code-prefix extraction) is tested directly, so a refactor that puts prose back in front of the code fails here. Say the word if you would rather have them internal and asserted end-to-end.

static let noRoutesFoundCode = "noroutesfound"

/// A provider-level failure rendered in the same `"<code>: <detail>"` shape the top-level
/// `error` field uses, so both reach `message(for:coin:minimum:)` through one path.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: Correct the nonexistent mapper reference in the doc comment

The comment references message(for:coin:minimum:), but this module only defines message(for:coin:). Point the documentation at the actual mapper so readers are not directed to a nonexistent overload.

Suggested change
/// `error` field uses, so both reach `message(for:coin:minimum:)` through one path.
/// `error` field uses, so both reach `message(for:coin:)` through one path.

source: ['codex']

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 46ebf78. The same slip existed a second time — isBelowMinimum(_:) referred to [isAmountTooLow], which your other comment asked me to delete — so both now point at symbols that exist.

Comment on lines +55 to +65
/// True when the failure means "the sell amount is under what this route can fill" — the case
/// the amount screens surface inline (raise the amount and retry) instead of as a dead end.
/// `noRoutesFound` is included because SwapKit answers with it for amounts far below the
/// minimum and only switches to an explicit below-minimum code close to the floor: measured
/// 2026-08-28, DASH → BTC returned `noRoutesFound` at 0.01 DASH and `sellAssetAmountTooSmall`
/// (min 0.175) at 0.05. It is genuinely ambiguous — a route can also be briefly unavailable —
/// so its copy stays neutral.
static func isAmountTooLow(_ rawError: String?) -> Bool {
let code = code(of: rawError)
return code == noRoutesFoundCode || isBelowMinimumCode(code)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: Remove the unused isAmountTooLow(_:) helper

Repository-wide usage shows no caller for this newly added helper. The amount screens call message(for:coin:), while routability intentionally uses the narrower isNoRoute(_:) and isBelowMinimum(_:) methods. Delete the unused API and move any useful ambiguity explanation to the classifier that implements the behavior.

source: ['codex']

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 46ebf78. It was added for parity with Android’s SwapKitErrors.isAmountTooLow, but nothing here calls it: the amount screens go through message(for:coin:) and routability deliberately uses the two narrower tests. The measured ambiguity it documented moved onto isNoRoute(_:), which is the classifier that acts on it.

Review follow-up.

A top-level `noRoutesFound` was cached as a conclusive negative for ten
minutes, but SwapKit answers with it for an amount far below a route's
floor as well as for a pair it cannot carry — measured 2026-08-28,
DASH -> BTC returned it at 0.01 DASH and quoted a route at 0.3. The
probe is a $50 estimate that falls back to one whole unit when no USD
price is cached, so a cheap asset could be probed under its own floor
and disappear from the picker for the rest of the window. Only a
provider naming its own no-route in `providerErrors[]` stays conclusive:
it answers for the single provider the probe asked about.

Also from review: drop `isAmountTooLow(_:)`, which had no callers once
the amount screens moved to `message(for:coin:)` and routability took
the two narrower tests; move the ambiguity it documented onto
`isNoRoute(_:)`, which is what acts on it. Point two doc comments at
symbols that exist.

Adds regression coverage for the normalization rules: code-before-prose
composition and its fallbacks, blank and nil handling, the
case-insensitive below-minimum suffix family, the four newly mapped
codes, the shared memo-too-long copy, unknown-code fallback, and the
anchor case — prose without its code must not be mistaken for a mapping.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants